/*
 * $Id$
 *
 * Copyright 2009 Sun Microsystems, Inc., 4150 Network Circle,
 * Santa Clara, California 95054, U.S.A. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */
package org.jdesktop.swingx.plaf.basic.core;

import javax.swing.DefaultListSelectionModel;
import javax.swing.ListModel;
import javax.swing.ListSelectionModel;
import javax.swing.RowSorter;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.RowSorterEvent;
import javax.swing.event.RowSorterListener;

import org.jdesktop.swingx.JXList;
import org.jdesktop.swingx.SwingXUtilities;
import org.jdesktop.swingx.util.Contract;

//import sun.swing.SwingUtilities2;

/**
 * ListSortUI provides support for managing the synchronization between
 * RowSorter, SelectionModel and ListModel if a JXList is sortable.
 * <p>
 * 
 * This implementation is an adaption of JTable.SortManager fit to the needs of
 * a ListUI. In contrast to JTable tradition, the ui delegate has full control
 * about listening to model/selection changes and updating the list accordingly.
 * So it's role is that of a helper to the ui-delgate (vs. as a helper of the
 * JTable). It's still up to the ListUI itself to listen to model/selection and
 * propagate the notification to this class, if a sorter is installed, but still
 * do the usual updates (layout, repaint) itself. On the other hand, listening
 * to the sorter and updating list state accordingly is completely done by this.
 * 
 */
public final class ListSortUI {
	private RowSorter<? extends ListModel> sorter;
	private JXList list;

	// Selection, in terms of the model. This is lazily created
	// as needed.
	private ListSelectionModel modelSelection;
	private int modelLeadIndex;
	// Set to true while in the process of changing the selection.
	// If this is true the selection change is ignored.
	private boolean syncingSelection;
	// Temporary cache of selection, in terms of model. This is only used
	// if we don't need the full weight of modelSelection.
	private int[] lastModelSelection;
	private boolean sorterChanged;
	private boolean ignoreSortChange;
	private RowSorterListener sorterListener;

	/**
	 * Intanstiates a SortUI on the list which has the given RowSorter.
	 * 
	 * @param list
	 *            the list to control, must not be null
	 * @param sorter
	 *            the rowSorter of the list, must not be null
	 * @throws NullPointerException
	 *             if either the list or the sorter is null
	 * @throws IllegalStateException
	 *             if the sorter is not the sorter installed on the list
	 */
	public ListSortUI(JXList list, RowSorter<? extends ListModel> sorter) {
		this.sorter = Contract.asNotNull(sorter, "RowSorter must not be null");
		this.list = Contract.asNotNull(list, "list must not be null");
		if (sorter != list.getRowSorter())
			throw new IllegalStateException("sorter must be same as the one on list");
		sorterListener = createRowSorterListener();
		sorter.addRowSorterListener(sorterListener);
	}

	/**
	 * Disposes any resources used by this SortManager. Note: this instance must
	 * not be used after dispose!
	 */
	public void dispose() {
		if (sorter != null) {
			sorter.removeRowSorterListener(sorterListener);
		}
		sorter = null;
		list = null;
	}

	// ----------------------methods called by listeners

	/**
	 * Called after notification from ListModel.
	 * 
	 * @param e
	 *            the change event from the listModel.
	 */
	public void modelChanged(ListDataEvent e) {
		ModelChange change = new ModelChange(e);
		prepareForChange(change);
		notifySorter(change);
		if (change.type != ListDataEvent.CONTENTS_CHANGED) {
			// If the Sorter is unsorted we will not have received
			// notification, force treating insert/delete as a change.
			sorterChanged = true;
		}
		processChange(change);
	}

	/**
	 * Called after notification from selectionModel.
	 * 
	 * Invoked when the selection, on the view, has changed.
	 */
	public void viewSelectionChanged(ListSelectionEvent e) {
		if (!syncingSelection && modelSelection != null) {
			modelSelection = null;
		}
	}

	/**
	 * Called after notification from RowSorter.
	 * 
	 * @param e
	 *            RowSorter event of type SORTED.
	 */
	protected void sortedChanged(RowSorterEvent e) {
		sorterChanged = true;
		if (!ignoreSortChange) {
			prepareForChange(e);
			processChange(null);
			// PENDING Jw: this is fix of 1161-swingx - not updated after
			// setting
			// rowFilter
			// potentially costly? but how to distinguish a mere sort from a
			// filterchanged? (only the latter requires a revalidate)
			// first fix had only revalidate/repaint but was not
			// good enough, see #1261-swingx - no items visible
			// after setting rowFilter
			// need to invalidate the cell size cache which might be needed
			// even after plain sorting as the indi-sizes are now at different
			// positions
			list.invalidateCellSizeCache();
		}
	}

	// --------------------- prepare change, that is cache selection if needed
	/**
	 * Invoked when the RowSorter has changed. Updates the internal cache of the
	 * selection based on the change.
	 * 
	 * @param sortEvent
	 *            the notification
	 * @throws NullPointerException
	 *             if the given event is null.
	 */
	private void prepareForChange(RowSorterEvent sortEvent) {
		Contract.asNotNull(sortEvent, "sorter event not null");
		// sort order changed. If modelSelection is null and filtering
		// is enabled we need to cache the selection in terms of the
		// underlying model, this will allow us to correctly restore
		// the selection even if rows are filtered out.
		if (modelSelection == null && sorter.getViewRowCount() != sorter.getModelRowCount()) {
			modelSelection = new DefaultListSelectionModel();
			ListSelectionModel viewSelection = getViewSelectionModel();
			int min = viewSelection.getMinSelectionIndex();
			int max = viewSelection.getMaxSelectionIndex();
			int modelIndex;
			for (int viewIndex = min; viewIndex <= max; viewIndex++) {
				if (viewSelection.isSelectedIndex(viewIndex)) {
					modelIndex = convertRowIndexToModel(sortEvent, viewIndex);
					if (modelIndex != -1) {
						modelSelection.addSelectionInterval(modelIndex, modelIndex);
					}
				}
			}
			modelIndex = convertRowIndexToModel(sortEvent, viewSelection.getLeadSelectionIndex());
			SwingXUtilities.setLeadAnchorWithoutSelection(modelSelection, modelIndex, modelIndex);
		} else if (modelSelection == null) {
			// Sorting changed, haven't cached selection in terms
			// of model and no filtering. Temporarily cache selection.
			cacheModelSelection(sortEvent);
		}
	}

	/**
	 * Invoked when the list model has changed. This is invoked prior to
	 * notifying the sorter of the change. Updates the internal cache of the
	 * selection based on the change.
	 * 
	 * @param change
	 *            the notification
	 * @throws NullPointerException
	 *             if the given event is null.
	 */
	private void prepareForChange(ModelChange change) {
		Contract.asNotNull(change, "table event not null");
		if (change.allRowsChanged) {
			// All the rows have changed, chuck any cached selection.
			modelSelection = null;
		} else if (modelSelection != null) {
			// Table changed, reflect changes in cached selection model.
			switch (change.type) {
				case ListDataEvent.INTERVAL_REMOVED:
					modelSelection.removeIndexInterval(change.startModelIndex, change.endModelIndex);
					break;
				case ListDataEvent.INTERVAL_ADDED:
					modelSelection.insertIndexInterval(change.startModelIndex, change.endModelIndex, true);
					break;
				default:
					break;
			}
		} else {
			// table changed, but haven't cached rows, temporarily
			// cache them.
			cacheModelSelection(null);
		}
	}

	private void cacheModelSelection(RowSorterEvent sortEvent) {
		lastModelSelection = convertSelectionToModel(sortEvent);
		modelLeadIndex = convertRowIndexToModel(sortEvent, getViewSelectionModel().getLeadSelectionIndex());
	}

	// ----------------------- process change, that is restore selection if
	// needed
	/**
	 * Inovked when either the table has changed or the sorter has changed and
	 * after the sorter has been notified. If necessary this will reapply the
	 * selection and variable row heights.
	 */
	private void processChange(ModelChange change) {
		if (change != null && change.allRowsChanged) {
			allChanged();
			getViewSelectionModel().clearSelection();
		} else if (sorterChanged) {
			restoreSelection(change);
		}
	}

	/**
	 * Restores the selection from that in terms of the model.
	 */
	private void restoreSelection(ModelChange change) {
		syncingSelection = true;
		if (lastModelSelection != null) {
			restoreSortingSelection(lastModelSelection, modelLeadIndex, change);
			lastModelSelection = null;
		} else if (modelSelection != null) {
			ListSelectionModel viewSelection = getViewSelectionModel();
			viewSelection.setValueIsAdjusting(true);
			viewSelection.clearSelection();
			int min = modelSelection.getMinSelectionIndex();
			int max = modelSelection.getMaxSelectionIndex();
			int viewIndex;
			for (int modelIndex = min; modelIndex <= max; modelIndex++) {
				if (modelSelection.isSelectedIndex(modelIndex)) {
					viewIndex = sorter.convertRowIndexToView(modelIndex);
					if (viewIndex != -1) {
						viewSelection.addSelectionInterval(viewIndex, viewIndex);
					}
				}
			}
			// Restore the lead
			int viewLeadIndex = modelSelection.getLeadSelectionIndex();
			if (viewLeadIndex != -1) {
				viewLeadIndex = sorter.convertRowIndexToView(viewLeadIndex);
			}
			SwingXUtilities.setLeadAnchorWithoutSelection(viewSelection, viewLeadIndex, viewLeadIndex);
			viewSelection.setValueIsAdjusting(false);
		}
		syncingSelection = false;
	}

	/**
	 * Restores the selection after a model event/sort order changes. All
	 * coordinates are in terms of the model.
	 */
	private void restoreSortingSelection(int[] selection, int lead, ModelChange change) {
		// Convert the selection from model to view
		for (int i = selection.length - 1; i >= 0; i--) {
			selection[i] = convertRowIndexToView(change, selection[i]);
		}
		lead = convertRowIndexToView(change, lead);

		// Check for the common case of no change in selection for 1 row
		if (selection.length == 0 || (selection.length == 1 && selection[0] == list.getSelectedIndex())) {
			return;
		}
		ListSelectionModel selectionModel = getViewSelectionModel();
		// And apply the new selection
		selectionModel.setValueIsAdjusting(true);
		selectionModel.clearSelection();
		for (int i = selection.length - 1; i >= 0; i--) {
			if (selection[i] != -1) {
				selectionModel.addSelectionInterval(selection[i], selection[i]);
			}
		}
		SwingXUtilities.setLeadAnchorWithoutSelection(selectionModel, lead, lead);
		selectionModel.setValueIsAdjusting(false);
	}

	// ------------------- row index conversion methods
	/**
	 * Converts a model index to view index. This is called when the sorter or
	 * model changes and sorting is enabled.
	 *
	 * @param change
	 *            describes the TableModelEvent that initiated the change; will
	 *            be null if called as the result of a sort
	 */
	private int convertRowIndexToView(ModelChange change, int modelIndex) {
		if (modelIndex < 0) {
			return -1;
		}
		// Contract.asNotNull(change, "change must not be null?");
		if (change != null && modelIndex >= change.startModelIndex) {
			if (change.type == ListDataEvent.INTERVAL_ADDED) {
				if (modelIndex + change.length >= change.modelRowCount) {
					return -1;
				}
				return sorter.convertRowIndexToView(modelIndex + change.length);
			} else if (change.type == ListDataEvent.INTERVAL_REMOVED) {
				if (modelIndex <= change.endModelIndex) {
					// deleted
					return -1;
				} else {
					if (modelIndex - change.length >= change.modelRowCount) {
						return -1;
					}
					return sorter.convertRowIndexToView(modelIndex - change.length);
				}
			}
			// else, updated
		}
		if (modelIndex >= sorter.getModelRowCount()) {
			return -1;
		}
		return sorter.convertRowIndexToView(modelIndex);
	}

	private int convertRowIndexToModel(RowSorterEvent e, int viewIndex) {
		// JW: the event is null if the selection is cached in prepareChange
		// after model notification. Then the conversion from the
		// sorter is still valid as the prepare is called before
		// notifying the sorter.
		if (e != null) {
			if (e.getPreviousRowCount() == 0) {
				return viewIndex;
			}
			// range checking handled by RowSorterEvent
			return e.convertPreviousRowIndexToModel(viewIndex);
		}
		// Make sure the viewIndex is valid
		if (viewIndex < 0 || viewIndex >= sorter.getViewRowCount()) {
			return -1;
		}
		return sorter.convertRowIndexToModel(viewIndex);
	}

	/**
	 * Converts the selection to model coordinates. This is used when the model
	 * changes or the sorter changes.
	 */
	private int[] convertSelectionToModel(RowSorterEvent e) {
		int[] selection = list.getSelectedIndices();
		for (int i = selection.length - 1; i >= 0; i--) {
			selection[i] = convertRowIndexToModel(e, selection[i]);
		}
		return selection;
	}

	// ------------------
	/**
	 * Notifies the sorter of a change in the underlying model.
	 */
	private void notifySorter(ModelChange change) {
		try {
			ignoreSortChange = true;
			sorterChanged = false;
			if (change.allRowsChanged) {
				sorter.allRowsChanged();
			} else {
				switch (change.type) {
					case ListDataEvent.CONTENTS_CHANGED:
						sorter.rowsUpdated(change.startModelIndex, change.endModelIndex);
						break;
					case ListDataEvent.INTERVAL_ADDED:
						sorter.rowsInserted(change.startModelIndex, change.endModelIndex);
						break;
					case ListDataEvent.INTERVAL_REMOVED:
						sorter.rowsDeleted(change.startModelIndex, change.endModelIndex);
						break;
				}
			}
		} finally {
			ignoreSortChange = false;
		}
	}

	private ListSelectionModel getViewSelectionModel() {
		return list.getSelectionModel();
	}

	/**
	 * Invoked when the underlying model has completely changed.
	 */
	private void allChanged() {
		modelLeadIndex = -1;
		modelSelection = null;
	}

	// ------------------- implementing listeners

	/**
	 * Creates and returns a RowSorterListener. This implementation calls
	 * sortedChanged if the event is of type SORTED.
	 * 
	 * @return rowSorterListener to install on sorter.
	 */
	protected RowSorterListener createRowSorterListener() {
		RowSorterListener l = new RowSorterListener() {

			@Override
			public void sorterChanged(RowSorterEvent e) {
				if (e.getType() == RowSorterEvent.Type.SORTED) {
					sortedChanged(e);
				}

			}

		};
		return l;
	}

	/**
	 * ModelChange is used when sorting to restore state, it corresponds to data
	 * from a TableModelEvent. The values are precalculated as they are used
	 * extensively.
	 * <p>
	 * 
	 * PENDING JW: this is not yet fully adapted to ListDataEvent.
	 */
	final static class ModelChange {
		// JW: if we received a dataChanged, there _is no_ notion
		// of end/start/length of change
		// Starting index of the change, in terms of the model, -1 if
		// dataChanged
		int startModelIndex;

		// Ending index of the change, in terms of the model, -1 if dataChanged
		int endModelIndex;

		// Length of the change (end - start + 1), - 1 if dataChanged
		int length;

		// Type of change
		int type;

		// Number of rows in the model
		int modelRowCount;

		// True if the event indicates all the contents have changed
		boolean allRowsChanged;

		public ModelChange(ListDataEvent e) {
			type = e.getType();
			modelRowCount = ((ListModel) e.getSource()).getSize();
			startModelIndex = e.getIndex0();
			endModelIndex = e.getIndex1();
			allRowsChanged = startModelIndex < 0;
			length = allRowsChanged ? -1 : endModelIndex - startModelIndex + 1;
		}
	}

}
